﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Collections;

namespace DimMock
{
    internal static class Mockers
    {
        /// <summary>
        /// Mocks random values for the given object.
        /// </summary>
        /// <typeparam name="T">The object type to mock.</typeparam>
        /// <param name="objectToMock">The object to mock.</param>
        /// <param name="allowNulls">if set to <c>true</c> will randomly create random values.</param>
        /// <returns>An object with random values applied.</returns>
        public static MockType Mock<MockType>(this MockType objectToMock, bool allowNulls = false)
        {
            if (objectToMock == null)
                throw new ArgumentNullException("objectToMock");

            IEnumerable<PropertyInfo> properties = typeof(MockType).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite);
            if (properties != null && properties.Any())
            {
                foreach (var property in properties)
                {
                    if (!allowNulls || Random.Boolean())
                    {
                        // support for Nullable<>
                        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                        {
                            Type nullableType = property.PropertyType.GetGenericArguments().FirstOrDefault();
                            if (nullableType == null)
                                throw new MockRandomValueException("Could not determine base type from nullable property.");

                            // this passes the underlying nullable type, rather than the property type.
                            property.GetSetMethod().Invoke(objectToMock, new Object[] { MockValue(nullableType) });
                        }
                        else if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
                        {
                            // coming soon
                        }
                        else if (!property.PropertyType.IsGenericType)
                        {
                            if (typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == property.PropertyType).Any())
                                property.GetSetMethod().Invoke(objectToMock, new Object[] { MockValue(property.PropertyType) });
                            else if (property.PropertyType.IsClass)
                                property.GetSetMethod().Invoke(objectToMock, new Object[] { DimMock.Mock.Create(property.PropertyType, true, allowNulls) });
                        }
                    }
                }
            }
            else if(typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == typeof(MockType)).Any())
            {
                MethodInfo method = typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == typeof(MockType)).FirstOrDefault();
                    objectToMock = (MockType) method.Invoke(null, new Object[] { });
            }

            return objectToMock;
        }

        /// <summary>
        /// Mocks a given class instance.
        /// </summary>
        /// <param name="objectToMock">The object to mock.</param>
        /// <param name="allowNulls">if set to <c>true</c> [allow nulls].</param>
        /// <returns></returns>
        public static dynamic MockClass(dynamic objectToMock, bool allowNulls = false)
        {
            if (objectToMock == null)
                throw new ArgumentNullException("objectToMock");

            Type mockType = objectToMock.GetType();

            IEnumerable<PropertyInfo> properties = mockType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite);
            if (properties != null && properties.Any())
            {
                foreach (var property in properties)
                {
                    if (!allowNulls || Random.Boolean())
                    {
                        // support for Nullable<>
                        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                        {
                            Type nullableType = property.PropertyType.GetGenericArguments().FirstOrDefault();
                            if (nullableType == null)
                                throw new MockRandomValueException("Could not determine base type from nullable property.");

                            // this passes the underlying nullable type, rather than the property type.
                            property.GetSetMethod().Invoke(objectToMock, new Object[] { MockValue(nullableType) });
                        }
                        // For now we ignore generics.
                        else if (!property.PropertyType.IsGenericType)
                        {
                            if (typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == property.PropertyType).Any())
                                property.GetSetMethod().Invoke(objectToMock, new Object[] { MockValue(property.PropertyType) });
                            else if (property.PropertyType.IsClass)
                                property.GetSetMethod().Invoke(objectToMock, new Object[] { DimMock.Mock.Create(property.PropertyType, true, allowNulls) });
                        }
                    }
                }
            }
            else if (typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == mockType).Any())
            {
                MethodInfo method = typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == mockType).FirstOrDefault();
                objectToMock = method.Invoke(null, new Object[] { });
            }

            return objectToMock;
        }

        /// <summary>
        /// Creates a random mock value for the targetType specified.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <returns></returns>
        public static dynamic MockValue(Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException("targetType");

            MethodInfo randomMethod = typeof(Random).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(meth => meth.ReturnType == targetType).FirstOrDefault();
            if(randomMethod == null)
                throw new MockRandomValueException("Could not mock random value for type \"{0}\".  If this is a custom type, implement the partial class \"{1}\" with the following method: \"public static {0} {0}(){{ ... }}\"", targetType.Name, typeof(Random).Name);

            return randomMethod.Invoke(null, null);
        }
    }
}
